/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package it.tukano.swingdeco.draganddrop;

import it.tukano.collections.CollectionsExt;
import it.tukano.collections.MapList;
import it.tukano.fun.Function1;
import it.tukano.fun.Function2;
import it.tukano.fun.Function3;
import it.tukano.fun.VFunction2;
import it.tukano.fun.VFunction3;
import it.tukano.io.FileExt;
import java.awt.Component;
import java.awt.Image;
import java.awt.Point;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragGestureEvent;
import java.awt.dnd.DragGestureListener;
import java.awt.dnd.DragSource;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetAdapter;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;

/**
 * Drag and drop utilities
 * @author pgi
 */
public class DndUtilities {

    /**
     * Creates a set of uris from a transferable holding a string
     * @param t the string or file list transferable
     * @return the list of uris
     */
    static URI[] createUris(Transferable t) {
        ArrayList<URI> uris = new ArrayList<URI>();
        if (t.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
            try {
                List<?> files = List.class.cast(t.getTransferData(DataFlavor.javaFileListFlavor));
                for (Object object : files) {
                    File file = (File) object;
                    uris.add(file.toURI());
                }
            } catch (UnsupportedFlavorException ex) {
                Logger.getLogger(DndUtilities.class.getName()).log(Level.SEVERE, null, ex);
            } catch (IOException ex) {
                Logger.getLogger(DndUtilities.class.getName()).log(Level.SEVERE, null, ex);
            }
        } else if (t.isDataFlavorSupported(DataFlavor.stringFlavor)) {
            try {
                String text = (String) t.getTransferData(DataFlavor.stringFlavor);
                Scanner in = new Scanner(text);
                while (in.hasNextLine()) {
                    String line = in.nextLine();
                    if (line.toLowerCase().startsWith("file")) {
                        try {
                            uris.add(new URI(line));
                        } catch (URISyntaxException ex) {
                            Logger.getLogger(DndUtilities.class.getName()).log(Level.INFO, ex.getMessage());
                        }
                    }
                }
            } catch (UnsupportedFlavorException ex) {
                Logger.getLogger(DndUtilities.class.getName()).log(Level.SEVERE, null, ex);
            } catch (IOException ex) {
                Logger.getLogger(DndUtilities.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        return uris.toArray(new URI[uris.size()]);
    }

    /**
     * Creates a transferable wrapping an object instance
     * @param flavor the data flavor to used
     * @param instance the instance to wrap
     * @return a transferable holding the given instance
     */
    public static Transferable newLocalObjectTransferable(final DataFlavor flavor, final Object instance) {
        return new Transferable() {

            public DataFlavor[] getTransferDataFlavors() {
                return new DataFlavor[]{flavor};
            }

            public boolean isDataFlavorSupported(DataFlavor f) {
                return flavor.equals(f);
            }

            public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
                if (isDataFlavorSupported(flavor)) {
                    return instance;
                } else {
                    throw new UnsupportedFlavorException(flavor);
                }
            }
        };
    }

    /**
     * Installs a drag source in the given component that transfers jvm local istances of a given class
     * @param <T> the type of the instance to transfer
     * @param source the source of the drag event
     * @param objectType the type of object
     * @param generator a function called when the dnd system detects a drag event. The function must
     * return null if there is nothing to transfer or an instance of the object type class to start dragging. The first
     * parameter of the function will be the component that initiated the drag action.
     */
    public static <T> void installLocalObjectDragHandler(final Component source,
            final Class<T> objectType, final Function2<Component, Point, T> generator) {
//java.awt.datatransfer.DataFlavor[mimetype=application/x-java-serialized-object;representationclass=java.lang.String]
        final DataFlavor flavor;
        try {
            flavor = new DataFlavor(DataFlavor.javaJVMLocalObjectMimeType);
        } catch (ClassNotFoundException ex) {
            Logger.getLogger(DndUtilities.class.getName()).log(Level.SEVERE, null, ex);
            throw new RuntimeException(ex);
        }
        DragSource ds = DragSource.getDefaultDragSource();
        ds.createDefaultDragGestureRecognizer(source, DnDConstants.ACTION_MOVE, new DragGestureListener() {

            public void dragGestureRecognized(DragGestureEvent dge) {
                T instance = generator.apply(source, dge.getDragOrigin());
                if (instance != null) {
                    dge.startDrag(DragSource.DefaultMoveDrop, newLocalObjectTransferable(flavor, instance));
                }
            }
        });
    }

    /**
     * Installs a jvm local object drop handler in the target component.
     * @param <T> the type of the dropped component
     * @param target the component that receives the drop
     * @param objectType the class of the dropped object
     * @param callback the function that will be called when the dnd system detects a valid drop
     */
    public static <T> void installLocalObjectDropHandler(Component target, final Class<T> objectType, final Function1<T, ?> callback) {
        VFunction2<Component, T> fun = new VFunction2<Component, T>() {

            @Override
            public void doApply(Component a, T b) {
                callback.apply(b);
            }
        };
        DndUtilities.installLocalObjectDropHandler(target, objectType, fun);

    }

    /**
     * Installs a generic drop handler (handles both local and non local transfers
     * @param target the target component
     * @param supportedTypes the supported types (use File for files, Image for image files or images)
     * @param callback the callback function (target component, drop location transferred values by type)
     */
    public static void installDropHandler(Component target, Iterable<Class<?>> supportedTypes, final Function3<Component, Point, Map<Class<?>, Object>, ?> callback) {
        final MapList<DataFlavor, Class<?>> flavors = new MapList<DataFlavor, Class<?>>();
        for (Class<?> type : supportedTypes) {
            if (type == Image.class) {
                flavors.append(DataFlavor.imageFlavor, type);
            } else if (type == File.class) {
                flavors.append(DataFlavor.javaFileListFlavor, type);
                flavors.append(DataFlavor.stringFlavor, type);
            } else {
                //flavors.append(new DataFlavor(type, type.getSimpleName()), type);
            }
        }
        DropTarget dropTarget = new DropTarget(target, new DropTargetAdapter() {

            public void drop(DropTargetDropEvent e) {
                Component component = e.getDropTargetContext().getComponent();
                DataFlavor flavor = null;
                for (DataFlavor dataFlavor : flavors.keySet()) {
                    if (e.isDataFlavorSupported(dataFlavor)) {
                        flavor = dataFlavor;
                        break;
                    }
                }
                if (flavor != null) {
                    e.acceptDrop(DnDConstants.ACTION_LINK);
                    List<Class<?>> types = flavors.get(flavor);
                    if (types != null) {
                        Class<?> transferType = types.get(0);
                        if (transferType == File.class) {
                            handleFileTransfer(e, component, callback);
                        } else if (transferType == Image.class) {
                            handleImageTransfer(e, component, callback);
                        } else {
                            handleLocalObjectTransfer(flavor, e, callback);
                        }
                    }
                    e.dropComplete(true);
                } else try {
                    DataFlavor local = new DataFlavor(DataFlavor.javaJVMLocalObjectMimeType);
                    if (e.isDataFlavorSupported(local)) {
                        handleLocalObjectTransfer(local, e, callback);
                    } else {
                        e.rejectDrop();
                    }
                } catch (ClassNotFoundException ex) {
                    Logger.getLogger(DndUtilities.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        });
    }

    private static void handleLocalObjectTransfer(DataFlavor flavor, DropTargetDropEvent e, Function3<Component, Point, Map<Class<?>, Object>, ?> callback) {
        try {
            Object transferData = e.getTransferable().getTransferData(flavor);
            Map<Class<?>, Object> map = new HashMap<Class<?>, Object>();
            map.put(transferData.getClass(), transferData);
            callback.apply(e.getDropTargetContext().getComponent(), e.getLocation(), map);
        } catch (UnsupportedFlavorException ex) {
            Logger.getLogger(DndUtilities.class.getName()).log(Level.SEVERE, null, ex);
        } catch (IOException ex) {
            Logger.getLogger(DndUtilities.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    private static void handleImageTransfer(DropTargetDropEvent e, Component component, Function3<Component, Point, Map<Class<?>, Object>, ?> callback) {
        try {
            //image object
            Image image = (Image) e.getTransferable().getTransferData(DataFlavor.imageFlavor);
            Map<Class<?>, Object> map = new HashMap<Class<?>, Object>();
            map.put(Image.class, image);
            callback.apply(component, e.getLocation(), map);
        } catch (UnsupportedFlavorException ex) {
            Logger.getLogger(DndUtilities.class.getName()).log(Level.SEVERE, null, ex);
        } catch (IOException ex) {
            Logger.getLogger(DndUtilities.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    private static void handleFileTransfer(DropTargetDropEvent e, Component component, Function3<Component, Point, Map<Class<?>, Object>, ?> callback) {
        //file object
        URI[] uris = createUris(e.getTransferable());
        Map<Class<?>, Object> map = new HashMap<Class<?>, Object>();
        for (int i = 0; i < uris.length; i++) {
            URI uri = uris[i];
            map.put(File.class, new File(uri));
            callback.apply(component, e.getLocation(), map);
        }
    }

    /**
     * Installs a drop handler that manages multiple types of local objects.
     * @param target the target component
     * @param types an iterable of all the local object types catched by the drop handler
     * @param callback the function called when a drop occurs. The function receives the
     * drop component, the drop location and map of class-instance values. The map stores
     * the dropped instances paired with the appropriate type.
     */
    public static void installLocalObjectDropHandler(Component target, Iterable<Class<?>> types, final Function3<Component, Point, Map<Class<?>, Object>, ?> callback) {
        final List<DataFlavor> flavors = CollectionsExt.simpleCollect(types, new Function1<Class<?>, DataFlavor>() {

            public DataFlavor apply(Class<?> p) {
                return new DataFlavor(p, p.getSimpleName());
            }
        });//TODO fix this as installDropHandler
        DropTarget dropTarget = new DropTarget(target, new DropTargetAdapter() {

                public void drop(DropTargetDropEvent e) {
                    DataFlavor flavor = null;
                    for (DataFlavor dataFlavor : flavors) {
                        if (e.isDataFlavorSupported(dataFlavor)) {
                            flavor = dataFlavor;
                            break;
                        }
                    }
                    if (flavor != null) {
                        e.acceptDrop(DnDConstants.ACTION_MOVE);
                        Class<?> type = flavor.getRepresentationClass();
                        try {
                            Object transferData = e.getTransferable().getTransferData(flavor);
                            Map<Class<?>, Object> map = new HashMap<Class<?>, Object>();
                            map.put(type, transferData);
                            callback.apply(e.getDropTargetContext().getComponent(), e.getLocation(), map);
                        } catch (UnsupportedFlavorException ex) {
                            Logger.getLogger(DndUtilities.class.getName()).log(Level.SEVERE, null, ex);
                        } catch (IOException ex) {
                            Logger.getLogger(DndUtilities.class.getName()).log(Level.SEVERE, null, ex);
                        }
                        e.dropComplete(true);
                    } else {
                        e.rejectDrop();
                    }
                }
            });
    }

    /**
     * Installs a drop handler in the given component
     * @param <T> the type of the dropped value
     * @param target the drop target component
     * @param objectType the type of the dropped instances
     * @param callback the function that will be notified when a drop occurs. The function
     * will receive the drop component, the drop location and the transferred instance
     */
    public static <T> void installLocalObjectDropHandler(Component target, final Class<T> objectType, final Function3<Component, Point, T, ?> callback) {
        final DataFlavor flavor = new DataFlavor(objectType, objectType.getSimpleName());
        new DropTarget(target, new DropTargetAdapter() {

            public void drop(DropTargetDropEvent e) {
                if (e.isDataFlavorSupported(flavor)) {
                    e.acceptDrop(DnDConstants.ACTION_MOVE);
                    try {
                        Object object = e.getTransferable().getTransferData(flavor);
                        callback.apply(e.getDropTargetContext().getDropTarget().getComponent(), e.getLocation(), objectType.cast(object));
                    } catch (UnsupportedFlavorException ex) {
                        Logger.getLogger(DndUtilities.class.getName()).log(Level.SEVERE, null, ex);
                    } catch (IOException ex) {
                        Logger.getLogger(DndUtilities.class.getName()).log(Level.SEVERE, null, ex);
                    }
                    e.dropComplete(true);
                } else {
                    e.rejectDrop();
                }
            }
        });
    }

    /**
     * Installs a jvm local object drop handler in the target component
     * @param <T> the type of the dropped value
     * @param target the target component
     * @param objectType the class of the dropped value
     * @param callback the function that will be called when the user drops an instance in the target component. The first parameter
     * of the function is the receiving component.
     */
    public static <T> void installLocalObjectDropHandler(Component target, final Class<T> objectType, final Function2<Component, T, ?> callback) {
        final DataFlavor flavor = new DataFlavor(objectType, objectType.getSimpleName());
        new DropTarget(target, new DropTargetAdapter() {

            public void drop(DropTargetDropEvent e) {
                if (e.isDataFlavorSupported(flavor)) {
                    e.acceptDrop(DnDConstants.ACTION_LINK);
                    try {
                        Object object = e.getTransferable().getTransferData(flavor);
                        callback.apply(e.getDropTargetContext().getDropTarget().getComponent(), objectType.cast(object));
                    } catch (UnsupportedFlavorException ex) {
                        Logger.getLogger(DndUtilities.class.getName()).log(Level.SEVERE, null, ex);
                    } catch (IOException ex) {
                        Logger.getLogger(DndUtilities.class.getName()).log(Level.SEVERE, null, ex);
                    }
                    e.dropComplete(true);
                } else {
                    e.rejectDrop();
                }
            }
        });
    }

    /**
     * Installs a file drop listener on the target component. Reacts to java file and uri lists
     * @param target the target component
     * @param filter the filter function that will check the accepted files
     * @param callback the function that will be invoked when a drop is detected
     */
    public static void installFileDropHandler(final Component target, final Function1<FileExt, Boolean> filter, final Function2<FileExt, Component, ?> callback) {
        new DropTarget(target, new DropTargetAdapter() {

            public void drop(DropTargetDropEvent e) {
                if (e.isDataFlavorSupported(DataFlavor.javaFileListFlavor) || e.isDataFlavorSupported(DataFlavor.stringFlavor)) {
                    e.acceptDrop(DnDConstants.ACTION_LINK);
                    URI[] uris = createUris(e.getTransferable());
                    for (URI uri : uris) {
                        FileExt file = new FileExt(new File(uri));
                        if (filter.apply(file)) {
                            callback.apply(file, target);
                        }
                    }
                    e.dropComplete(true);
                }
            }
        });
    }

    /**
     * Add a drop handler that listens for file drops (files or uri list)
     * @param target the drop target
     * @param filter the function that filters out unsupported file
     * @param callback the function that handles the drop (dropped file, drop target, drop location on target)
     */
    public static void installFileDropHandler(final Component target, final Function1<FileExt, Boolean> filter, final Function3<FileExt, Component, Point, ?> callback) {
        new DropTarget(target, new DropTargetAdapter() {

            public void drop(DropTargetDropEvent e) {
                if (e.isDataFlavorSupported(DataFlavor.javaFileListFlavor) || e.isDataFlavorSupported(DataFlavor.stringFlavor)) {
                    e.acceptDrop(DnDConstants.ACTION_LINK);
                    URI[] uris = createUris(e.getTransferable());
                    for (URI uri : uris) {
                        FileExt file = new FileExt(new File(uri));
                        if (filter.apply(file)) {
                            callback.apply(file, target, e.getLocation());
                        }
                    }
                    e.dropComplete(true);
                }
            }
        });
    }

    /**
     * Installs an image drop listener on the target component. Reacts to java images and
     * image files
     * @param target the drop target
     * @param callback the function that will be called when the user drops a java image or a supported
     * image file on the target component.
     */
    public static void installImageDropHandler(Component target, final Function1<Image, ?> callback) {
        VFunction2<Component, Image> call = new VFunction2<Component, Image>() {

            @Override
            public void doApply(Component a, Image b) {
                callback.apply(b);
            }
        };
        DndUtilities.installImageDropHandler(target, call);
    }

    /**
     * Installs an image drop listener on the target component. Reacts to java images and image files
     * @param target the drop target
     * @param callback the function that will be called when the user drops a java image or a supported image file
     * on the target component.
     */
    public static void installImageDropHandler(Component target, final Function2<Component, Image, ?> callback) {
        final VFunction3<Component, Point, Image> callbackWrapper = new VFunction3<Component, Point, Image>() {

            @Override
            protected void doApply(Component p1, Point p2, Image p3) {
                callback.apply(p1, p3);
            }
        };
        new DropTarget(target, new DropTargetAdapter() {

            public void drop(DropTargetDropEvent e) {
                if (e.isDataFlavorSupported(DataFlavor.imageFlavor)) {
                    e.acceptDrop(DnDConstants.ACTION_LINK);
                    DndUtilities.doTransferImage(e, callback);
                    e.dropComplete(true);
                } else if (e.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
                    e.acceptDrop(DnDConstants.ACTION_LINK);
                    DndUtilities.doTransferImageFiles(e, callbackWrapper);
                    e.dropComplete(true);
                } else if (e.isDataFlavorSupported(DataFlavor.stringFlavor)) {
                    e.acceptDrop(DnDConstants.ACTION_LINK);
                    DndUtilities.doTransferImageUris(e, callbackWrapper);
                    e.dropComplete(true);
                } else {
                    e.rejectDrop();
                }
            }
        });
    }

    /**
     * Generates a uri list from a list of lines.
     * @param lines the list of lines to scan
     * @return the uris listed in the given array of string, if any
     */
    public static URI[] createUris(String[] lines) {
        ArrayList<URI> uris = new ArrayList<URI>();
        if (lines != null) {
            for (int i = 0; i < lines.length; i++) {
                String line = lines[i].trim();
                try {
                    uris.add(new URI(line));
                } catch (URISyntaxException ex) {
                    Logger.getLogger(DndUtilities.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        }
        return uris.toArray(new URI[uris.size()]);
    }

    static void doTransferImageUris(DropTargetDropEvent e, Function3<Component, Point, Image, ?> callback) {
        try {
            String[] tokens = e.getTransferable().getTransferData(DataFlavor.stringFlavor).toString().split("\n");
            URI[] uris = createUris(tokens);
            String[] readerFileSuffixes = ImageIO.getReaderFileSuffixes();
            for (URI uri : uris) {
                try {
                    FileExt file = new FileExt(new File(uri));
                    if (file.hasExtension(readerFileSuffixes)) {
                        BufferedImage image = ImageIO.read(file.getFile());
                        callback.apply(e.getDropTargetContext().getDropTarget().getComponent(), e.getLocation(), image);
                    }
                } catch (IOException ex) {
                    Logger.getLogger(DndUtilities.class.getName()).log(Level.INFO, ex.getMessage());
                }
            }
        } catch (UnsupportedFlavorException ex) {
            Logger.getLogger(DndUtilities.class.getName()).log(Level.SEVERE, null, ex);
        } catch (IOException ex) {
            Logger.getLogger(DndUtilities.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    static void doTransferImage(DropTargetDropEvent e, Function2<Component, Image, ?> callback) {
        try {
            Image image = (Image) e.getTransferable().getTransferData(DataFlavor.imageFlavor);
            callback.apply(e.getDropTargetContext().getDropTarget().getComponent(), image);
        } catch (UnsupportedFlavorException ex) {
            Logger.getLogger(DndUtilities.class.getName()).log(Level.SEVERE, null, ex);
        } catch (IOException ex) {
            Logger.getLogger(DndUtilities.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    static void doTransferImageFiles(DropTargetDropEvent e, Function3<Component, Point, Image, ?> callback) {
        String[] readerFileSuffixes = ImageIO.getReaderFileSuffixes();
        try {
            List<?> files = List.class.cast(e.getTransferable().getTransferData(DataFlavor.javaFileListFlavor));
            for (Object object : files) {
                FileExt file = new FileExt((File) object);
                if (file.hasExtension(readerFileSuffixes)) {
                    BufferedImage image = ImageIO.read(file.getFile());
                    callback.apply(e.getDropTargetContext().getDropTarget().getComponent(), e.getLocation(), image);
                }
            }
        } catch (UnsupportedFlavorException ex) {
            Logger.getLogger(DndUtilities.class.getName()).log(Level.SEVERE, null, ex);
        } catch (IOException ex) {
            Logger.getLogger(DndUtilities.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    public static void installFileDragger(Component source, final Function1<Point, List<File>> producer) {
        DragSource ds = DragSource.getDefaultDragSource();
        ds.createDefaultDragGestureRecognizer(source, DnDConstants.ACTION_COPY_OR_MOVE, new DragGestureListener() {

            public void dragGestureRecognized(DragGestureEvent e) {
                Point dragOrigin = e.getDragOrigin();
                final List<?> files = Collections.unmodifiableList(producer.apply(dragOrigin));
                if(files != null && !files.isEmpty()) {
                    Transferable t = new Transferable() {

                        public DataFlavor[] getTransferDataFlavors() {
                            return new DataFlavor[] { DataFlavor.javaFileListFlavor };
                        }

                        public boolean isDataFlavorSupported(DataFlavor flavor) {
                            return flavor.equals(DataFlavor.javaFileListFlavor);
                        }

                        public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
                            if(!isDataFlavorSupported(flavor)) throw new UnsupportedFlavorException(flavor);
                            return files;
                        }
                    };
                    e.startDrag(DragSource.DefaultMoveDrop, t);
                }
            }
        });
    }
}
